iT邦幫忙

2026 iThome 鐵人賽

DAY 28
0
Modern Web

用 Astro 打造 Content-first 前端網站:30 天從靜態內容到會員、資料庫與選型(3rd)系列 第 28

Astro 內容站該選 Google Sheets、Keystatic 還是 Strapi?

  • 分享至 

  • xImage
  •  

先看內容正本與編輯流程,再選 CMS。Google
Sheets 適合小型、欄位固定的表格資料;Keystatic 適合想保留 Markdown 與 Git,又需要編輯介面的團隊;Strapi 則把內容模型、權限與發布流程做得更完整,代價是多維護一套 server 和資料庫。

三種方案都能把內容交給 Astro。選型要比較誰負責資料、編輯者怎麼工作,以及內容何時算發布;fetch()
的寫法不是主要差異。技術基準是 Astro 7.1.1,官方文件查證與實測日期為 2026-07-24。

先把共同流程畫出來

不論內容放在哪,build-time Content Layer 的流程都是:

編輯者
  → 內容正本(Sheet/Git repo/Strapi DB)
  → Astro loader
  → schema 驗證與 Content Layer store
  → build
  → 靜態頁面

loader 解決「資料怎麼進 Astro」,不會接管原始資料的 ownership。Sheet 仍由 Google
Drive 管,Keystatic 的檔案仍在 repo,Strapi 的內容仍在自己的 DB。Astro store 是供查詢與建置使用的衍生資料。

來源改了,線上靜態頁不會跟著更新。build-time loader 只在 content
sync/build 時讀來源;要發布變更,仍需由 webhook 或 CI 再跑一次 build。這跟
Day 15:SSG 與 SSR的頁面產生時機是同一個取捨。

Astro 7 也有 request-time 的 live loader,但它會把遠端 API 的延遲、quota 與可用性放進每次請求,並失去 runtime MDX
render 和 Astro 圖片最佳化。這個內容站的文章、RSS、JSON 都已採 build-time collection,因此繼續沿用同一個執行模型。

三種方案的工作流放在一起

方案 工作流與代價
Google Sheets 正本在 Google Drive,試算表使用者可以直接編輯;缺少內容模型與 Draft & Publish,更新後要重新 build。
Keystatic 正本是 Git repo 裡的內容檔,schema 與 branch 流程留在 codebase;線上 Admin 會多出認證與 server runtime。
Strapi 5 正本在 Strapi DB,後台提供 relations、media 與 Draft & Publish;團隊要維護 CMS server、DB、備份與升級。

這張表裡沒有「全面最好」的方案。內容正本想留在 Git 時,Strapi 就算功能最多,也不構成搬進 DB 的理由;團隊若需要正式 draft、relations 與角色分工,再用 Sheet 補欄位慣例也不合適。

許多專案一開始不用 CMS,內容正本直接放在程式碼裡。

一個上線中的多語系品牌官網走的是這條路。它的文案分成兩處:四份語系 JSON 放 UI 字串與段落文案,其餘標題與圖片直接硬編碼在元件的 template 裡。編輯流程等於改 code、送 PR、重新部署。

專案早期這樣做有其道理。文案量少、只有工程師在改,也還沒有編輯者要進來,多一套 CMS 就會多一套維護工作。等編輯者變成非工程師,或語系增加,這套做法的代價就會出現。這個專案兩件事都發生了:共有四個語系,而且 JSON 的 key 數量已經不一致,英文 89 個、其他三個語系各 83 個。少掉的那六個字串沒有任何機制會提醒;缺 key 時,取值函式會回傳字串
NONE,直接渲染到正式頁面上。

內容正本與編輯流程決定了要補哪個缺口。上表三個方案各自對應不同需求:Sheets 是「讓非工程師能編輯」,Keystatic 是「保留 Git 正本但給編輯介面」,Strapi 是「內容模型、權限與發布狀態」。無論選哪一個,schema 驗證那一關都不能省(Day 12 那條線):正本搬到哪裡,缺欄位都應該讓 build 失敗,不能讓讀者看到
NONE

該不該把內容從 code 裡搬出來,可以先問「誰要改它」和「改完誰負責確認沒缺」,不必先數文案有幾行。只要其中一題的答案不是工程師,就值得比較上表的方案。

Google Sheets:編輯門檻低,內容約束也最少

Google
Sheets 省掉一個學習門檻:編輯者已經會用。共享、留言、版本紀錄與權限都在現成介面裡,欄位固定的名單、推薦語、活動時程或價格表,不必先教一套 CMS。

Astro 沒有官方 Google Sheets loader。正式接法通常是 custom loader 呼叫
spreadsheets.values.get,把第一列當欄位、後續每列轉成 entry,再交給 Zod 驗證。私有 Sheet 可用 OAuth 或 service
account;只讀情境應採最窄的 read-only scope。

代價是內容規則幾乎都要自己補:

  • Sheet 沒有 content type、relation、slug 治理或 Draft & Publish。
  • 儲存格可以被任何有編輯權的人改成錯誤型別,必須由 Astro schema 在 build 時擋下。
  • 公開 CSV 等於公開資料。私人名單、token、內部價格不能放進這條路徑。
  • Google Sheets API 有 read
    quota;官方目前列出每 project 每分鐘 300 次、每 user/project 每分鐘 60 次。build-time 一次批次讀取,比每個 request 都去查 Sheet 更容易控制。

所以 Sheets 適合「幾列結構化內容需要低門檻協作」,不適合作為完整文章 CMS。

Keystatic:Git 繼續當正本,前面多一個編輯介面

Keystatic 的方向不同。它用 schema 產生 Admin UI,內容仍可存成 Markdown、Markdoc、MDX、YAML 或 JSON。local
mode 寫本機檔案,GitHub mode 直接讀寫 repo,Keystatic Cloud 也仍連到指定的 GitHub repo。

這種模式適合已採用 Markdown 與 Git review,又希望寫作者不必每次進 IDE 改 frontmatter 的團隊;Markdown 的可攜性可參考
Day 9:Markdown 的可攜性。內容 diff、branch 與 rollback 仍沿用既有工具。

不過,2026-07-24 的官方 README 仍把 Keystatic 標為
experimental。專案沒有停更,也已加入 Astro
7 支援,但「活躍維護」不等於「穩定承諾已完成」。

導入 Keystatic 前,還要確認部署條件。官方 Astro 安裝會加入 React、Markdoc、@keystatic/core@keystatic/astro,production
Admin 需要 server-side code 與 Node.js APIs。這個 capstone 的優先部署環境是 Cloudflare
Workers,而 Keystatic 官方沒有對應的 Workers production 指引。若只在本機 local
mode 編輯,可以在 production 關掉 Admin;若要提供線上編輯,還得確認認證與 Node runtime 的部署位置。

這個實作沒有安裝 Keystatic。只有一個遠端結構化來源,還不足以換整套編輯 runtime。

Strapi:完整內容後台,也是一套獨立系統

Strapi 5 會替 content type 產生 REST endpoints,支援 filter、sort、pagination、fields、relations、locale 與
status=draft|published。Content Manager 能管理 components、dynamic zones、media;Draft & Publish 也能依 content
type 開啟。

這些能力適合多人內容團隊。編輯者不需要理解 repo,內容模型與發布狀態也不靠試算表欄位暗號維持。對 Astro 而言,接法仍可是一個 custom
loader:build 時呼叫 Strapi REST API、驗證 response,再寫入 Content Layer。

Strapi 要另外啟動一個 Node server,加上一個 SQL DB;自架時要處理備份、升級、監控與 API token。Content
types 預設 private,必須設定 public permission 或提供正確權限的 token。靜態 Astro 頁要在 publish 後更新,還要用
Strapi webhook觸發 CI 或部署。

如果需求已經包含多角色、relations、media 與正式 preview,維護 Strapi 的成本才合理。若內容只有六張資料卡,Strapi 帶來的維運工作反而多過內容管理需求。

用四個問題做選型

問題 答案偏向
編輯者只需要改固定欄位,而且已熟悉試算表? Google Sheets
內容要留在 repo,Git diff/branch 是既有發布流程? Keystatic
需要 relations、media、角色權限與 Draft & Publish? Strapi
團隊能不能維護 production Admin runtime 或 CMS server/DB? 不能時,先選更小的方案

選型時先回答這四個 workflow 問題。答案清楚後,再比較套件版本、費用與 UI,這個順序能避免許多「裝完才發現正本放錯地方」的重工。

最小實作:Google Sheet 進 Content Layer

最小實作以 Google Sheets 驗證遠端表格內容,30 篇文章仍留在 Git。資料源是 Google 官方 quickstart 的 sample
spreadsheet,公開 CSV 共有 30 筆人物資料。

資料進獨立的 sheetProfiles collection:

import { defineCollection } from "astro:content";
import { googleSheetProfileLoader } from "./loaders/google-sheet-profile-loader";

const sheetProfiles = defineCollection({
  loader: googleSheetProfileLoader({
    url: "https://docs.google.com/spreadsheets/d/1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms/gviz/tq?tqx=out:csv&sheet=Class%20Data",
  }),
});

export const collections = {
  sheetProfiles,
};

loader 先檢查 HTTP,再驗證前六個表頭。每列要經兩層驗證:來源 tuple 確認 cell shape,parseData() 再套用 collection
schema。最後才寫進 store:

let rows = parseCsv(await response.text());
let headers = rows.shift();

if (!headers) {
  throw new Error("Google Sheet CSV did not contain a header row.");
}
assertExpectedHeaders(headers);

for (let [index, sourceRow] of rows.entries()) {
  let parsedRow = googleSheetSourceRowSchema.parse(sourceRow);
  let [studentName, gender, classLevel, homeState, major, activity] = parsedRow;
  let id = createProfileId(studentName);
  let data = await parseData({
    id,
    data: {
      studentName,
      gender,
      classLevel,
      homeState,
      major,
      activity,
      rowNumber: index + 2,
      sourceUrl: sourceUrl.href,
    },
  });

  store.set({ id, data, digest: generateDigest(data) });
}

這個 sample 的姓名不重複,範例才用姓名產 ID;loader 遇到重複 ID 會直接讓 build 失敗。正式內容應在 Sheet 加一欄不隨標題變動的
id。否則編輯者改名就會讓 entry identity 一起改,內鏈或引用也跟著失效。

CSV parser 只負責格式。實作用一個小型 state machine 處理引號、逗號、CRLF 與 quoted
newline;資料規模更大或格式更複雜時,換成熟 CSV library 即可,collection consumer 不用變。

為什麼不把 Sheet 塞進 blog

目前 blog
collection 會同時餵首頁、搜尋、分頁、文章 route、Day 20 的 RSS 與 JSON。它的 schema 還要求
titledescriptiondaypubDate 與可渲染正文。

sample spreadsheet 沒有這些欄位。若仍轉成 blog entry,30 筆人物資料會進入文章列表、RSS 與搜尋,直接污染既有 consumer。

既有 blog schema 維持不變,只新增一個 data collection 與 /demos/cms-content-source。這個分層沿用
Day 10:Content CollectionsDay 11:custom loader
Day 12:schema 驗證的做法:

---
import { getCollection } from 'astro:content';

const profiles = (await getCollection('sheetProfiles')).toSorted( (a, b) => a.data.rowNumber - b.data.rowNumber, );
---

<ol>
  { profiles.slice(0, 6).map((profile) => (
  <li>{profile.data.studentName} · {profile.data.major}</li>
  )) }
</ol>

來源換成 Sheet 後,頁面仍是普通 Astro template,也不需要 Vue island。

Google Sheets 內容來源證據頁顯示 sheetProfiles collection 共 30 筆,頁面列出前 6 筆人物資料

實測:從 config 到 browser

實測沿「來源 → store → output → browser」逐層核對:

驗收層 結果
Google Sheet CSV 30 筆資料列,前六欄表頭符合預期
Content Layer sheetProfiles 30 entries
靜態頁 顯示前 6 張卡,順序對應 Sheet row 2–7
Client island <astro-island> 0 個
1440px 桌面 innerWidth = scrollWidth = 1440
390px 手機 innerWidth = scrollWidth = 390,卡片為單欄
Accessibility axe WCAG A/AA:0 violations、0 incomplete

頁面仍有全站共用的 <ClientRouter /> scripts;本次只能確認「這個 CMS demo 沒有 client island
hydration」,不能說整頁零 JavaScript。

最終 build 也產出 29 篇文章、5 個分頁、RSS、JSON 與 Day 11 loader demo。loader
demo 是 29/3/1:相較 baseline 的 28/3/1,只多了本篇 blog entry,30 筆 sheetProfiles 沒有混進三張既有 loader 卡。

遠端來源還有三個失敗邊界

第一,遠端 CSV 連不到、HTTP 非 2xx 或表頭被改掉,content
sync 會失敗,整次靜態 build 也會停。這是刻意的 fail-fast:比部署一份欄位錯位的頁面安全。

第二,Sheet 更新後仍要 rebuild。若要自動化,可把編輯流程接到 CI;Google Sheets 沒有像 Strapi publish
event 那樣直接的 CMS webhook,通常要用 Apps Script 或排程補觸發機制。

第三,公開資料與正式私有內容要分開。這個 demo 使用 Google 官方 sample,不含專案私人資料;內部 Sheet 應改走 Sheets
API、service account、read-only scope,secret 只放 build environment。處理 secret 的方式可沿用
Day 22:server env 邊界,不能把憑證寫進 loader URL 或 client bundle。

結論:用 workflow 決定方案

Google Sheets 的編輯門檻最低,Keystatic 為 Git-backed
content 加上編輯 UI。Strapi 提供完整內容平台,但 server 與 DB 也會進入維運範圍。

小型結構化內容先用 Sheet,Git 是內容正本時看 Keystatic,正式內容團隊再評估 Strapi。無論選哪一個,都要先確認三件事:內容由誰擁有、如何驗證、哪個事件才算發布。

這次用 Google Sheets 完成「遠端來源 → Content Layer → 靜態頁面」,既有 blog consumer 也維持不變。下一篇 Day
30 會回到整個 capstone:什麼專案適合 Astro,什麼情況別硬上。


今日驗收:Google 官方 sample spreadsheet 以 custom loader 載入 sheetProfiles 30
entries;/demos/cms-content-source 顯示前 6 筆、client island hydration 為 0,1440px/390px 無水平溢位,axe WCAG
A/AA 為 0 violations,Astro production build 通過。


上一篇
astro.config 裡的 integrations 到底在做什麼?該裝哪些?
系列文
用 Astro 打造 Content-first 前端網站:30 天從靜態內容到會員、資料庫與選型(3rd)28
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言